﻿Option Explicit On
Option Strict On
Option Infer On

Imports System
Imports System.Xml.Linq
Imports Microsoft.Build.Framework
Imports Microsoft.Build
Imports Microsoft.Build.BuildEngine
Imports <xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

Public MustInherit Class AdjustBase
   Inherits Microsoft.Build.Utilities.Task

   Private mTargetDirectory As String
   Private mTemplateFilter As String
   Private mCodeStart As String
   Private mCodeEnd As String

   Protected MustOverride Function AdjustTemplate( _
            ByVal fileName As String) _
            As String
   Protected MustOverride ReadOnly Property AdjustSuffix() As String

   ''' <summary>
   ''' This optional value can be used to indicate a non-standard code start/end marker set 
   ''' </summary>
   ''' <value>The marker that indicates the start of a code block</value>
   ''' <returns>The marker that indicates the start of a code block</returns>
   ''' <remarks>
   ''' Anticipated use is from MS Build/project file script
   ''' <para/>
   ''' Ensure that the code start and markers are different and never 
   ''' appear except in the context of code start/end
   ''' </remarks>
   Public Property CodeStartMarker() As String
      Get
         If String.IsNullOrEmpty(mCodeStart) Then
            Return "<code>"
         End If
         Return mCodeStart
      End Get
      Set(ByVal value As String)
         mCodeStart = value
      End Set
   End Property

   ''' <summary>
   ''' This optional value can be used to indicate a non-standard code start/end marker set 
   ''' </summary>
   ''' <value>The marker that indicates the end of a code block</value>
   ''' <returns>The marker that indicates the end of a code block</returns>
   ''' <remarks>
   ''' Anticipated use is from MS Build/project file script
   ''' <para/>
   ''' Ensure that the code start and markers are different and never 
   ''' appear except in the context of code start/end
   ''' </remarks>
   Public Property CodeEndMarker() As String
      Get
         If String.IsNullOrEmpty(mCodeStart) Then
            Return "</code>"
         End If
         Return mCodeStart
      End Get
      Set(ByVal value As String)
         mCodeEnd = value
      End Set
   End Property

   ''' <summary>
   ''' Templates are identified by a well known naming pattern. The default pattern is that
   ''' the name ends in .Template.vb, case insensitive. Any alternate case insensitive pattern
   ''' can be used. The filter is a regex pattern
   ''' </summary>
   ''' <value>The filter used to recognize templates</value>
   ''' <returns>The filter used to recognize templates</returns>
   ''' <remarks>
   ''' Anticipated use is from MS Build/project file script
   ''' <para/>
   ''' Templates are evaluated. All other files are just copied to the output directory
   ''' </remarks>
   Public Property TemplateFilter() As String
      Get
         If String.IsNullOrEmpty(mTemplateFilter) Then
            Return ".\.template.vb"
         End If
         Return mTemplateFilter
      End Get
      Set(ByVal value As String)
         mTemplateFilter = value
      End Set
   End Property

   ''' <summary>
   ''' Files are copied into an intermediate location before being altered. This property
   ''' identifies this location. It currently must be a relative directory below the project root
   ''' </summary>
   ''' <value>The temporary output directory used during the adjustment process</value>
   ''' <returns>The temporary output directory used during the adjustment process</returns>
   ''' <remarks>
   ''' Anticipated use is from MS Build/project file script
   ''' </remarks>
   Public Property TargetDirectory() As String
      Get
         If String.IsNullOrEmpty(mTargetDirectory) Then
            Return "bin/Output"
         End If
         Return mTargetDirectory
      End Get
      Set(ByVal value As String)
         mTargetDirectory = value
      End Set
   End Property

   Public Sub LogNormalMessage(ByVal message As String)
      Me.Log.LogMessage(MessageImportance.Normal, message)
   End Sub

   Public Sub LogVerboseMessage(ByVal message As String)
      Me.Log.LogMessage(MessageImportance.Low, message)
   End Sub

   Public Sub LogWarning(ByVal message As String)
      Me.Log.LogWarning(message)
   End Sub

   Public Sub LogError(ByVal message As String)
      Me.Log.LogError(message)
   End Sub

   ''' <summary>
   ''' Perform the adjustment as defined in a derived class.
   ''' </summary>
   ''' <returns>True if the files were successfully copied and adjusted, otherwise false</returns>
   Public Overrides Function Execute() As Boolean
      Try
         Dim projectFilePath = Me.BuildEngine2.ProjectFileOfTaskNode
         Dim projectDirectory = IO.Path.GetFullPath(IO.Path.GetDirectoryName(projectFilePath))
         Dim outputDirectory = IO.Path.GetFullPath(IO.Path.Combine(projectDirectory, TargetDirectory))

         Me.Log.LogMessage(MessageImportance.Normal, "Adjusting project start:")
         Me.Log.LogMessage(MessageImportance.Normal, "     Output Directory = " & outputDirectory)
         Me.Log.LogMessage(MessageImportance.Normal, "     Project project = " & projectDirectory)

         Dim project = New Microsoft.Build.BuildEngine.Project()
         project.Load(projectFilePath)

         MakeNewProjectFile(project, projectFilePath, projectDirectory, outputDirectory)

         If Not IO.Directory.Exists(outputDirectory) Then
            IO.Directory.CreateDirectory(outputDirectory)
         End If

         AdjustItemFiles(project, "Compile", projectFilePath, projectDirectory, outputDirectory, True)
         AdjustItemFiles(project, "EmbeddedResource", projectFilePath, projectDirectory, outputDirectory, False)
         AdjustItemFiles(project, "None", projectFilePath, projectDirectory, outputDirectory, False)
         'AdjustProjectFile(project, projectDirectory, outputDirectory)
         'project.Save(newProjectFileName)

         Return True
      Catch ex As Exception
         Throw
      End Try

   End Function

   Private Sub AdjustItemFiles( _
            ByVal project As Project, _
            ByVal itemType As String, _
            ByVal projectFilePath As String, _
            ByVal projectDirectory As String, _
            ByVal outputDirectory As String, _
            ByVal doAdjust As Boolean)
      Dim regex = New Text.RegularExpressions.Regex(TemplateFilter, Text.RegularExpressions.RegexOptions.IgnoreCase)

      For Each item As BuildEngine.BuildItem In project.GetEvaluatedItemsByName(itemType)
         Dim name = item.FinalItemSpec
         Dim itemFullPath = IO.Path.Combine(projectDirectory, name)
         Dim itemOutputPath = IO.Path.Combine(outputDirectory, name)
         Dim itemOutputDirectory = IO.Path.GetDirectoryName(itemOutputPath)
         ' Do not copy links as it doesn't make much sense. Later we might adjust the links
         If (Aggregate metadataName In item.MetadataNames Where CStr(metadataName) = "Link" Into Count()) > 0 Then
            Continue For
         End If
         'Dim q = (From name In item.MetadataNames Where name = "Link").Count
         If Not IO.Directory.Exists(itemOutputDirectory) Then
            IO.Directory.CreateDirectory(itemOutputDirectory)
         End If
         If IO.File.Exists(itemOutputPath) AndAlso _
               ((IO.File.GetAttributes(itemOutputPath) And IO.FileAttributes.ReadOnly) = IO.FileAttributes.ReadOnly) Then
            IO.File.SetAttributes(itemOutputPath, IO.File.GetAttributes(itemOutputPath) And Not IO.FileAttributes.ReadOnly)
         End If
         Dim match = regex.IsMatch(name)
         If match Then
            Try
               Me.Log.LogMessage(MessageImportance.Normal, "     File: " & name)
               Dim code = AdjustTemplate(itemFullPath)
               IO.File.WriteAllText(itemOutputPath, code)
            Catch ex As Exception
               Throw
            End Try
         ElseIf itemFullPath <> projectFilePath Then
            IO.File.Copy(itemFullPath, itemOutputPath, True)
         End If
      Next
   End Sub

   Private Sub MakeNewProjectFile( _
            ByVal project As Project, _
            ByVal projectFilePath As String, _
            ByVal projectDirectory As String, _
            ByVal outputDirectory As String)
      Dim newProjectFileName = String.Empty
      Dim pathAdjust = GetPathAdjustment(projectDirectory, outputDirectory)
      Dim xDocument As XDocument = Nothing
      For Each Import As Import In project.Imports
         If Not Import.IsImported Then
            Dim path = Import.ProjectPath
            Dim root As XElement = Nothing
            If xDocument Is Nothing Then
               newProjectFileName = GetNewProjectName(path, outputDirectory)
               xDocument = xDocument.Load(path)
            Else
               Dim XElement As XElement = XElement.Load(path)
               For Each element In XElement.FirstNode.ElementsAfterSelf
                  xDocument.Add(element)
               Next
            End If
         End If
         Console.WriteLine(Import.ProjectPath)
      Next

      For Each element In xDocument...<AssemblyName>
         element.Add("." & AdjustSuffix)
      Next
      For Each element In xDocument...<OutputPath>
         element.Value = IO.Path.Combine(pathAdjust, element.Value)
      Next
      For Each element In xDocument...<ProjectReference>
         If element.@Include IsNot Nothing Then
            element.@Include = IO.Path.Combine(pathAdjust, element.@Include)
         End If
      Next
      For Each element In xDocument...<HintPath>
         element.Value = IO.Path.Combine(pathAdjust, element.Value)
      Next

      Dim removeElements = New List(Of XElement)
      For Each element In xDocument...<SccProjectName>
         removeElements.Add(element)
      Next
      For Each element In xDocument...<SccLocalPath>
         removeElements.Add(element)
      Next
      For Each element In xDocument...<SccAuxPath>
         removeElements.Add(element)
      Next
      For Each element In xDocument...<SccProvider>
         removeElements.Add(element)
      Next
      For Each element In removeElements
         element.Remove()
      Next

      If xDocument IsNot Nothing Then
         Dim dir = IO.Path.GetDirectoryName(newProjectFileName)
         If Not IO.Directory.Exists(dir) Then
            IO.Directory.CreateDirectory(dir)
         End If
         xDocument.Save(newProjectFileName)
      End If
   End Sub

   Private Function GetPathAdjustment( _
            ByVal directory1 As String, _
            ByVal directory2 As String) _
            As String
      Dim projectUri = New Uri(directory1 & If(directory1.EndsWith("/"), String.Empty, "/"))
      Dim outputUri = New Uri(directory2 & If(directory2.EndsWith("/"), String.Empty, "/"))
      Dim pathAdjustUri = outputUri.MakeRelativeUri(projectUri)
      Dim pathAdjust = pathAdjustUri.ToString()
      Return pathAdjust.Replace("/", "\")
   End Function


   Private Sub AdjustProjectFile( _
            ByVal project As Project, _
            ByVal projectDirectory As String, _
            ByVal outputDirectory As String)
      Dim projectUri = New Uri(projectDirectory & If(projectDirectory.EndsWith("/"), String.Empty, "/"))
      Dim outputUri = New Uri(outputDirectory & If(outputDirectory.EndsWith("/"), String.Empty, "/"))
      Dim pathAdjustUri = outputUri.MakeRelativeUri(projectUri)
      Dim pathAdjust = pathAdjustUri.ToString()

      ' TODO: Either remove explicit C# ref or move to C# file and make MustOverride
      Dim propertyGroups = project.PropertyGroups()
      For Each group As BuildPropertyGroup In project.PropertyGroups()
         For Each prop As BuildProperty In group
            If prop.IsImported Then
               Continue For
               'ElseIf prop.Name = "RootNamespace" Then
               '   prop.Value = prop.Value & "." & AdjustSuffix
            ElseIf prop.Name = "AssemblyName" Then
               prop.Value = prop.Value & "." & AdjustSuffix
            ElseIf prop.Name = "OutputPath" Then
               prop.Value = IO.Path.Combine(pathAdjust, prop.Value)
            ElseIf prop.Name = "BuildDependsOn" Then
               ' Remove this processing from the project file to avoid circularity. For now, I'm dropping back to the core processing
               ' I'm currently leaving the target in place to see what troubles it casues. 
               prop.Value = prop.Value.Replace("AdjustTemplatesAndRebuild;", "")
            End If
         Next
      Next

      For Each group As BuildItemGroup In project.ItemGroups
         For Each item As BuildItem In group
            If item.Name = "ProjectReference" Then
               item.Include = IO.Path.Combine(pathAdjust, item.Include)
            ElseIf item.Name = "Reference" Then
               Dim hintPath = item.GetMetadata("HintPath")
               item.SetMetadata("HintPath", IO.Path.Combine(pathAdjust, hintPath))
            End If
         Next
      Next

      'For Each usingTask As UsingTask In project.UsingTasks
      '   ' This is not precise because it does not test that this word 
      '   ' is not contained in a bigger assembly name, but it should be adequate
      '   Dim assemblyName = Me.GetType.Assembly.GetName.Name
      '   If usingTask.AssemblyName.Contains(assemblyName) Then
      '      ' There does not appear to be a way to remove this. so I'm going to see if it causes problems.
      '      'project.UsingTasks.remove(usingTask)
      '   End If

      'Next
   End Sub

   Private Function GetNewProjectName( _
            ByVal projectFilePath As String, _
            ByVal outputDirectory As String) _
            As String
      Dim fileName As String = IO.Path.GetFileNameWithoutExtension(projectFilePath)
      Dim extension As String = IO.Path.GetExtension(projectFilePath)
      Return IO.Path.Combine(outputDirectory, (fileName & AdjustSuffix & extension))
   End Function

   'Private Function AdjustProjectFile( _
   '       ByVal projectDirectory As String, _
   '       ByVal outputDirectory As String) _
   '       As String
   '   projectDirectory = IO.Path.GetFullPath(projectDirectory)
   '   outputDirectory = IO.Path.GetFullPath(outputDirectory)
   '   Dim endMatchPos = 0
   '   For i = 0 To projectDirectory.Length - 1
   '      If i >= outputDirectory.Length Then
   '      ElseIf projectDirectory(i) <> outputDirectory(i) Then
   '         endMatchPos = i
   '         Exit For
   '      End If
   '   Next
   '   If endMatchPos = projectDirectory.Length Then
   '      ' the output direcory is a subset.
   '   End If
   'End Function

End Class
